/*
 *  Copyright 2011-2012 Redia A/S
 * 
 *  This file is part of Redias Android Utilities.
 *
 *  Redias Android Utilities is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Redias Android Utilities is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with Redias Android Utilities.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.redia.lib.net;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
import android.widget.ImageView;
import dk.redia.lib.log.Log;
import dk.redia.lib.processing.ManagedAsyncTask;
import dk.redia.lib.utils.AppApplication;
import dk.redia.lib.utils.DateUtils;
import dk.redia.lib.utils.ImageUrlUtils;
import dk.redia.lib.utils.ImageUtils;
import dk.redia.lib.utils.UIUtils;
import dk.redia.lib.utils.VersionUtils;

public class ImageCache {
	
	private static final String logTag = "ImageCache";
	private static ImageCache instance;
	private static int databaseVersion = 26;
	private static String databaseName = "images";
	
	private DisplayMetrics displayMetrics = new DisplayMetrics();
	
	private ImageCacheSQLiteOpenHelper dbHelper;
	//private int imageMaxSideLength;
	private int defaultEntryLifeTime = 60*60*6;
	
	public static ImageCache getInstance() {
		if (instance==null) {
			instance = new ImageCache();
		}
		return instance;
	}
	
	public ImageCache() {
		dbHelper = new ImageCacheSQLiteOpenHelper(AppApplication.getAppContext(), ImageCache.databaseName, null, ImageCache.databaseVersion);
		Context context = AppApplication.getAppContext();
		Display display = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
		display.getMetrics(displayMetrics);
		int width = displayMetrics.widthPixels;
		int height = displayMetrics.heightPixels;
		//imageMaxSideLength = Math.max(width, height)/3;
		Log.d(ImageCache.logTag, "Detected display size of " + width + "x" + height);
		SQLiteDatabase db = dbHelper.getWritableDatabase();
		db.beginTransaction();
		try {
			int pruneCount = db.delete("images", "updated<" + (DateUtils.getCurrentUnixTime()-defaultEntryLifeTime), new String[] {});
			db.setTransactionSuccessful();
			Log.d(logTag, "Pruned " + pruneCount + " images from cache during startup");
		} catch (SQLiteDiskIOException e) {
			// Silently ignore IO errors.
		} finally {
			try {
				db.endTransaction();
			} catch (SQLiteDiskIOException e) {
				// Silently ignore IO errors.
			}
		}
		db = dbHelper.getReadableDatabase();
		try {
			Cursor c = db.rawQuery("SELECT COUNT(url) AS count FROM images", new String[] {});
			if (c.moveToFirst()) {
				Log.d(logTag, "Cache size is " + c.getInt(c.getColumnIndex("count")) + " entries");
			}
		} catch (SQLiteDiskIOException e) {
			// Silently ignore IO errors.
		}
	}
	
	
//	public void loadImage(URL url, ImageCacheListener l) {
//		loadImage(url, l, null);
//	}
	
	private Bitmap applyEffects(Bitmap bmp, Bitmap effects, int targetWidth, int targetHeight) {
		if (bmp==null) {
			Log.w(ImageCache.logTag, "Not applying effects. Original image is null");
			return bmp;
		}
		if (effects==null) {
			Log.w(ImageCache.logTag, "Not applying effects, Effects image is null");
			return bmp;
		}
		Log.d(logTag, "Source image has size " + bmp.getWidth() + "x" + bmp.getHeight() + " target size is " + targetWidth + "x" + targetHeight);
		if (targetWidth!=-1 && targetHeight!=-1) {
			if (bmp.getWidth()!=targetWidth || bmp.getHeight()!=targetHeight) {
				Log.d(logTag, "Resizing source image to fit target rect");
				bmp = ImageUtils.resize(bmp, targetWidth, targetHeight, ImageUtils.MODE_CROP);
			}
		}
		
		Matrix resizeMatrix = new Matrix();
		float scaleX = ((float)bmp.getWidth())/((float)effects.getWidth());
		float scaleY = ((float)bmp.getHeight())/((float)effects.getHeight());
		resizeMatrix.postScale(scaleX, scaleY);
		Log.d(ImageCache.logTag, "Original image has size " + bmp.getWidth() + "x" + bmp.getHeight());
		Log.d(ImageCache.logTag, "Scaling image by " + scaleX + " / " + scaleY);
		Bitmap scaledEffectsBmp = Bitmap.createBitmap(effects, 0, 0, effects.getWidth(), effects.getHeight(), resizeMatrix, true);
		Log.d(ImageCache.logTag, "Created scaled effects bitmap of size " + scaledEffectsBmp.getWidth() + "x" + scaledEffectsBmp.getHeight());
    	Bitmap outputBmp = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888);
    	Canvas outputBmpCanvas = new Canvas(outputBmp);
    	Log.d(ImageCache.logTag, "Created output bitmap of size " + outputBmp.getWidth() + "x" + outputBmp.getHeight());
		outputBmpCanvas.drawBitmap(bmp, 0, 0, null);
		outputBmpCanvas.drawBitmap(scaledEffectsBmp, 0, 0, null);				
		return outputBmp;

	}
	
//	public void loadImage(URL url, ImageCacheListener l, Bitmap effects) {
//		loadImageImpl(new ResizedImageURL(url, imageMaxSideLength, imageMaxSideLength), l, effects, -1, -1);
//	}
	
	/**
	 * Load an image from specified URL resizing it to the specified size.
	 * Please note that size should be provided in DIP rather than PX.
	 * 
	 * @param url
	 * @param l
	 * @param effects
	 * @param dipWidth Density independent width of target
	 * @param dipHeight Density independent height of target
	 */
	public void loadImage(URL url, ImageCacheListener l, Bitmap effects, int dipWidth, int dipHeight) {
		int width = UIUtils.dipToPx(dipWidth);
		int height = UIUtils.dipToPx(dipHeight);
		loadImageImpl(new ResizedImageURL(url, width, height), l, effects, width, height);
	}
	
	public void loadImage(URL url, ImageCacheListener l, Bitmap effects, int dipWidth, int dipHeight, String resizeMode) {
		int width = UIUtils.dipToPx(dipWidth);
		int height = UIUtils.dipToPx(dipHeight);
		loadImageImpl(new ResizedImageURL(url, width, height, resizeMode), l, effects, width, height);
	}
	
	private void loadImageImpl(ResizedImageURL url, ImageCacheListener l, Bitmap effects, int width, int height) {
		Bitmap img = loadImageFromCache(url);
		if (img!=null) {
			// We got a valid result from the cache, so just provide that to the listener
			l.imageLoaded(applyEffects(img, effects, width, height));
		} else {
			// Image isn't cached, so we need to fetch it
			new HttpImageFetcher(l, effects, width, height).executeManaged(url);
		}		
	}
	
//	public void loadImage(URL url, ImageView view) {
//		ResizedImageURL rUrl = new ResizedImageURL(url, imageMaxSideLength, imageMaxSideLength);
//		loadImageToImageViewImpl(rUrl, view, null);
//	}
	
//	public void loadImage(Bitmap b, final ImageView view, Bitmap effects) {
//		view.setImageBitmap(applyEffects(b, effects, -1, -1));
//		view.invalidate();
//	}
	
	public void loadImage(Bitmap b, final ImageView view, Bitmap effects, int dipWidth, int dipHeight) {
		int height = UIUtils.dipToPx(dipHeight);
		int width = UIUtils.dipToPx(dipWidth);
		view.setImageBitmap(applyEffects(b, effects, width, height));
		view.invalidate();
	}
	
	private void loadImageToImageViewImpl(ResizedImageURL url, final ImageView view, Bitmap effects) {
			Log.d(ImageCache.logTag, "Downloading resized image from " + url.toString());
			loadImageImpl(url, new ImageCacheListener() {
				public void imageLoaded(Bitmap img) {
					if (img!=null) {
						view.setImageBitmap(img);
						view.invalidate();
					}
				}
			}, effects, -1, -1);
	}
	
	/**
	 * 
	 * @param url
	 * @param view
	 * @param effects
	 * @param dipWidth Target width in DIP, not PX
	 * @param dipHeight Target height in DIP, not PX
	 */
	public void loadImage(URL url, final ImageView view, Bitmap effects, int dipWidth, int dipHeight) {
		int height = UIUtils.dipToPx(dipHeight);
		int width = UIUtils.dipToPx(dipWidth);
		loadImageToImageViewImpl(new ResizedImageURL(url, width, height), view, effects);
	}
	
	public void loadImage(URL url, final ImageView view, Bitmap effects, int dipWidth, int dipHeight, String resizeMode) {
		int height = UIUtils.dipToPx(dipHeight);
		int width = UIUtils.dipToPx(dipWidth);
		loadImageToImageViewImpl(new ResizedImageURL(url, width, height, resizeMode), view, effects);
	}
	
	public void loadImage(URL url, final ImageView view, Bitmap effects, int dipWidth, int dipHeight, String resizeMode, String output) {
		int height = UIUtils.dipToPx(dipHeight);
		int width = UIUtils.dipToPx(dipWidth);
		loadImageToImageViewImpl(new ResizedImageURL(url, width, height, resizeMode, output), view, effects);
	}
	
	private Bitmap loadImageFromCache(ResizedImageURL url) {
		Log.d(ImageCache.logTag, "Loading bitmap from cache for URL " + url.toExternalForm());
		SQLiteDatabase db = dbHelper.getReadableDatabase();
		try {
			Cursor c = db.query("images", new String[] {"data"}, "url='" + url.toExternalForm() + "'", new String[] {}, null, null, null);
			if (c.moveToFirst()) {
				byte[] data = c.getBlob(c.getColumnIndex("data"));
				c.close();
				Log.d(ImageCache.logTag, "Cache hit: found " + data.length + " bytes of data in cache for URL " + url.toExternalForm());
				Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
				return image;
			} else {
				Log.d(ImageCache.logTag, "Cache miss: No image cache entry for " + url.toExternalForm());
				c.close();
				return null;
			}
		} catch (SQLiteDiskIOException e) {
			// Silently ignore IO errors.
			return null;
		}
	}

	private void addToCache(ResizedImageURL u, Bitmap result) {

		Log.d(ImageCache.logTag, "Writing bitmap to cache for URL " + u.toExternalForm());
		SQLiteDatabase db = dbHelper.getWritableDatabase();
		db.beginTransaction();
		try {
			int deleteCount = db.delete("images", "url='" + u.toExternalForm() + "'", new String[] { });
			Log.d(ImageCache.logTag, "Deleted " + deleteCount + " previously stored images for URL " + u.toExternalForm());
			ByteArrayOutputStream baos = new ByteArrayOutputStream();  
			result.compress(Bitmap.CompressFormat.PNG, 100, baos); //bm is the bitmap object   
			byte[] b = baos.toByteArray();  
			ContentValues values = new ContentValues();
			values.put("url", u.toExternalForm());
			values.put("updated", DateUtils.getCurrentUnixTime());
			values.put("data", b);
			db.insert("images", null, values);
			db.setTransactionSuccessful();
		} 
		catch (SQLiteDiskIOException e) {
			// Ignore this exception - there's nothing we can do about these i/o errors.
			Log.e(ImageCache.logTag, "I/O error while updating the image cache SQLite db");
		}
		finally {
			try { 
				db.endTransaction();
			} catch (SQLiteException e) {
				// Intentionally ignored.
				// Put here to handle case 7947
			}
		}
	}
	
	/**
	 * Simple wrapper around URL to keep track of
	 * when an image URL has been rewritten
	 * @author boll
	 *
	 */
	private class ResizedImageURL {
//		private URL originalUrl;
		private URL resizedUrl;
		
		public ResizedImageURL(URL u, int width, int height) {
//			originalUrl = u;
			Log.d(logTag, "Getting cropped image URL");
			resizedUrl = ImageUrlUtils.getResizedImageUrl(u, width, height, ImageUrlUtils.RESIZE_MODE_CROP_EDGES);
		}
		
		public ResizedImageURL(URL u, int width, int height, String resizeMode) {
//			originalUrl = u;
			resizedUrl = ImageUrlUtils.getResizedImageUrl(u, width, height, resizeMode);
		}
		
		public ResizedImageURL(URL u, int width, int height, String resizeMode, String output) {
//			originalUrl = u;
			resizedUrl = ImageUrlUtils.getResizedImageUrl(u, width, height, resizeMode, output);
		}
		
		public String toExternalForm() {
			return resizedUrl.toExternalForm();
		}
		
		public String toString() {
			return resizedUrl.toString();
		}

		public URLConnection openConnection() throws IOException {
			return resizedUrl.openConnection();
		}
	}
	
	private class ImageCacheSQLiteOpenHelper extends SQLiteOpenHelper {

		public ImageCacheSQLiteOpenHelper(Context context, String name, CursorFactory cf, int version) {
			super(context, name, cf, version);
		}
		
		@Override
		public void onCreate(SQLiteDatabase db) {
			Log.d(ImageCache.logTag, "Creating image cache database");
			try {
 				db.execSQL("CREATE TABLE IF NOT EXISTS images (" +
						"url TEXT PRIMARY KEY," +
						"updated INT(64)," +
						"data BLOB" +
						")");
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {
			Log.d(ImageCache.logTag, "Cleaning image cache database due to database upgrade");
			try {
				db.execSQL("DROP TABLE IF EXISTS images");
			} catch (SQLException e) {
				e.printStackTrace();
			}
			onCreate(db);
		}		
	}
	
	private class HttpImageFetcher extends ManagedAsyncTask<ResizedImageURL, Integer, Bitmap> {

		ImageCacheListener l;
		ResizedImageURL u;
		Bitmap effects;
		int width;
		int height;
//		boolean hadEffects = false;
		
		private HttpImageFetcher(ImageCacheListener l, Bitmap effects, int width, int height) {
			this.l = l;
			this.effects = effects;
			this.width = width;
			this.height = height;
//			hadEffects = effects!=null;
		}
		
		@Override
		protected Bitmap doInBackground(ResizedImageURL... urls) {
		    HttpURLConnection con = null;
		    Bitmap bmp = null;
		    if (urls.length>0) {
			    u = urls[0];
			    if (u!=null) {
				    Log.d(ImageCache.logTag, "Retrieving " + u.toExternalForm() + " from the network");
		
					try {
						con = (HttpURLConnection)u.openConnection(); // con doesn't become null, instead an IOException is thrown if stuff breaks
						con.setRequestProperty("User-Agent", "Android image cache/" + VersionUtils.getVersion() + "(" + VersionUtils.getVersionNickName() + ")");
						con.setUseCaches(false);
						con.connect();
//						Log.d(ImageCache.logTag, "Original URL has length " + u.toExternalForm().length());
//						Log.d(ImageCache.logTag, "URL in con has length " + con.getURL().toString().length());
						if (isCancelled()) {
							return bmp;
						}
					    InputStream is = con.getInputStream();
					    if (is!=null) {
						    bmp = BitmapFactory.decodeStream(is);
						    if (bmp!=null) {
							    Log.d(ImageCache.logTag, "Downloading image takes up " + (bmp.getRowBytes() * bmp.getHeight()) + " bytes of memory");
						    	Log.d(ImageCache.logTag, "Downloaded image with width=" + bmp.getWidth() + " px, height=" + bmp.getHeight() + " px from " + u.toExternalForm());
						    } else {
						    	Log.w(ImageCache.logTag, "Failed to download image from " + u.toExternalForm() + ". Unable to parse data as image.");
						    }
					    }
					} catch (IOException e) {
						e.printStackTrace();
					}
			    }
		    }
			
			return bmp;
		}
		@Override
		protected void onPostExecute(Bitmap bmp) {
			super.onPostExecute(bmp);
			if (bmp!=null) {
				addToCache(u, bmp);
			}
			if (effects==null) {
				l.imageLoaded(bmp);
			} else {
				l.imageLoaded(applyEffects(bmp, effects, width, height));	
			}
		}
		
	}
	
}
